Used list of priority species based on: TimeToRestore_AllPrioritySpecies_2024 google sheet (extracted on 2 April 2025) and made the following changes:
Corrected the spelling of one genus (Vernonia) and one species (cespitosa)
Updated scientific and common names based on ITIS for mistflower (Conoclinium), basket-flower (Centaurea), and Lantana; will use the common name listed in ITIS/NPN data base for these species imoving forward.
Using the common name listed in NPN database for Sambucus nigra and Passiflora incarnata, which were each listed twice in the googlesheet under two different common names.
Code
# Load list of priority speciesspp_list <-read.csv("data/ttr-priorityspecies-20250402.csv", na.strings =c(NA, ""))spp_list <- spp_list %>%mutate(across(c(common_name, scientific_name), str_trim)) %>%# Edit spelling of one genus (Vernonia) and one species (cespitosa)# Update species based on ITIS for mistflower (Conoclinium), # basket-flower (Centaurea), and Lantanamutate(scientific_name =case_when( scientific_name =="Oenothera caespitosa"~"Oenothera cespitosa", scientific_name =="Veronia gigantea"~"Vernonia gigantea", scientific_name =="Conoclinium greggii"~"Conoclinium dissectum", scientific_name =="Centaurea americana"~"Plectocephalus americanus", scientific_name =="Lantana urticoides"~"Lantana horrida", .default = scientific_name )) %>%rename(ttr_common_name = common_name)# Load information about NN speciesnn_spp <-npn_species() %>%data.frame()nn_spp <- nn_spp %>%filter(kingdom =="Plantae") %>%select(species_id, common_name, genus, species, functional_type) %>%mutate(scientific_name =paste(genus, species))# Find NN info based on scientific name of priority species (inconsistent # capitalization in priority list and a duplicate in NN database [Canada goldenrod])spp_list <- spp_list %>%left_join(nn_spp, by ="scientific_name")# Check that all priority species have a match in NN database# filter(spp_list, is.na(species_id))# Are there any duplicates? Yes# count(spp_list, species_id) %>% filter(n > 1)# filter(spp_list, species_id == 90)# In priority list, Sambucus nigra listed as both black and common elderberry# filter(spp_list, species_id == 182)# In priority list, Passiflora incarnata listed as both purple passionflower and Maypop# Remove entries for TTR priority species whose common name doesn't match NPNspp_list <- spp_list %>%filter(!ttr_common_name %in%c("Maypop", "Common elderberry"))
This left us with 53 priority species.
Downloading status-intensity data
Used rnpn package to download status and intensity data for priority species in Louisiana, New Mexico, Oklahoma, and Texas from 1 October 2023 through the present day. Downloaded observations of 4 phenophases: flowers or flower buds (flower), open flowers, fruits, and ripe fruits. Since leaves on milkweed plants may be important for monarch eggs and catepillars, we also downloaded observations of the leaves phenophase for the 8 species of milkweed on the priority list. After downloading the data, we appended information about intensity categories.
Code
# First, check that all species have these 4 phenophasesphenophases_byspp <-npn_phenophases_by_species(species_ids =c(spp_list$species_id),date ="2025-01-01") %>%data.frame()phenophases_byspp %>%group_by(species_id, species_name) %>%summarize(p500 =ifelse(500%in% phenophase_id, 1, 0),p501 =ifelse(501%in% phenophase_id, 1, 0),p516 =ifelse(516%in% phenophase_id, 1, 0),p390 =ifelse(390%in% phenophase_id, 1, 0),.groups ="keep") %>%rowwise() %>%filter(sum(c_across(p500:p390)) <4)# All species use these 4 phenophases# What phenophases do the milkweeds use?milkweed_phps <- phenophases_byspp %>%filter(str_detect(species_name, "milkweed")) %>%select(species_id, species_name, pheno_class_id, phenophase_id, phenophase_name) %>%arrange(species_name, pheno_class_id, phenophase_id)# Since leaves may be important for monarch eggs and catepillars, we may also # want to include:# 488 = Leaves (for milkweeds only)# Download and format (or load existing) NPN data for priority plant species --## We want observations in 4 states (LA, NM, OK, TX)# Focus on 2025 data, but also download 2024 data (for fruits and/or comparison)phenophases <-c(500, 501, 516, 390)states4 <-c("LA", "NM", "OK", "TX")# Note: we could be missing observations in the four states if we use# the states argument in the download function because sometimes the state # field is missing or incorrect. Best to download all records for species and# fixing/imputing state, and then filtering by state.data_filename <-"data/ttr-data-2024sep2025.csv"if (!file.exists(data_filename) | update_data ==TRUE) {# Download flowering, fruiting data for 2024-2025 (including 2023 so that # we can calculate individual phenometrics for 2024, if needed) status_dl <-npn_download_status_data(request_source ="erinz",years =2023:2025,species_ids = spp_list$species_id,phenophase_ids= phenophases,# states = states4,additional_fields =c("observedby_person_id","partner_group","site_name", "species_functional_type")) status_dl <-data.frame(status_dl)# Download leafing data for milkweeds in 2024-2025 milkweeds <- spp_list %>%filter(str_detect(common_name, "milkweed")) %>%pull(species_id) status_mwleaf_dl <-npn_download_status_data(request_source ="erinz",years =2023:2025,species_ids = milkweeds,phenophase_ids=488,# states = states4,additional_fields =c("observedby_person_id","partner_group","site_name", "species_functional_type")) status_mwleaf_dl <-data.frame(status_mwleaf_dl)# Combine everything and format status_df <-rbind(status_dl, status_mwleaf_dl) %>%mutate(obsdate =ymd(observation_date),yr =year(obsdate),php =case_when( phenophase_id ==500~"flower", phenophase_id ==501~"open flower", phenophase_id ==516~"fruit", phenophase_id ==390~"ripe fruit", phenophase_id ==488~"leaves")) %>%filter(obsdate >="2023-10-01") %>%select(-c(update_datetime, elevation_in_meters, genus, species, kingdom, phenophase_description, abundance_value, observation_date)) %>%rename(person_id = observedby_person_id,func_type = species_functional_type,lat = latitude,lon = longitude)# Some observations missing state ID. Will use a shapefile to get an assigned# state for each site and use that moving forward state_fill <- status_df %>%select(site_id, lon, lat, state) %>%distinct() state_fillv <-vect(state_fill, geom =c("lon", "lat"), crs ="epsg:4326") state_new <- terra::extract(states, state_fillv) state_fill <-cbind(state_fill, state_new = state_new$STUSPS)# check:# count(state_fill, state, state_new) %>%# mutate(same = ifelse(state == state_new, 1, 0)) %>%# arrange(same)# Attach new state labels and exclude observations that aren't in the 4 states status_df <- status_df %>%left_join(select(state_fill, site_id, state_new), by ="site_id") %>%select(-state) %>%rename(state = state_new) %>%filter(!is.na(state) & state %in% states4)# Write to filewrite.csv(status_df, data_filename, row.names =FALSE)# Remove objectsrm(status_df, status_dl, status_mwleaf_dl)}status <-read.csv(data_filename) %>%mutate(obsdate =ymd(obsdate),wy =ifelse(obsdate >="2024-10-01", 2025, 2024))
For the purposes of this report, we focused on observations submitted in the current water year, from 1 October 2024 - 18 September 2025, the last date that observations were available. Throughout this report, we often compared current water-year data with data collected during the previous water year (October 2023 - September 2024), while keeping in mind that we are not entirely through the current water year.
Code
# Download information about intensity categoriesic <-npn_abundance_categories() %>%data.frame()ic <- ic %>%rename(intensity_category_id = category_id, intensity_value_id = value_id,intensity_name = category_name,intensity_value = value_name) %>%select(-c(category_description, value_description))# Extract just those categories that appear in status data and format:ic_subset <- ic %>%filter(intensity_category_id %in%unique(status$intensity_category_id)) %>%mutate(value1 =NA,value2 =NA,intensity_type =case_when(str_detect(intensity_value, "%") ~"percent",str_detect(intensity_value, "[0-9]") ~"number",.default ="qualitative" ))val12 <-which(colnames(ic_subset) %in%c("value1", "value2"))for (i in1:nrow(ic_subset)) {if (str_detect(ic_subset$intensity_value[i], " to ")) { ic_subset[i, val12] <-str_split_fixed(ic_subset$intensity_value[i], " to ", 2) ic_subset[i, val12] <-as.numeric(str_remove(ic_subset[i, val12], ",")) } elseif (str_detect(ic_subset$intensity_value[i], "-")) { ic_subset[i, val12] <-str_split_fixed(ic_subset$intensity_value[i], "-", 2) ic_subset[i, val12[2]] <-str_remove(ic_subset[i, val12[2]], "%") } elseif (str_detect(ic_subset$intensity_value[i], "% or more")) { ic_subset[i, val12] <-str_remove(ic_subset$intensity_value[i], "% or more") } elseif (str_detect(ic_subset$intensity_value[i], "Less than ")) { ic_subset[i, val12[1]] <-0 ic_subset[i, val12[2]] <-str_remove(ic_subset$intensity_value[i], "Less than ") ic_subset[i, val12[2]] <-str_remove(ic_subset[i, val12[2]], "%") } elseif (str_detect(ic_subset$intensity_value[i], "More than ")) { ic_subset[i, val12] <-str_remove(ic_subset$intensity_value[i], "More than ") ic_subset[i, val12[1]] <-as.numeric(str_remove(ic_subset[i, val12[1]], ",")) +1 ic_subset[i, val12[2]] <-as.numeric(str_remove(ic_subset[i, val12[2]], ",")) +1 }}ic_subset <- ic_subset %>%mutate_at(c("value1", "value2"), as.numeric)# Assigning a middle-ish value for each range (keeping it to nice numbers like # 5, 50, 500, and 5000)ic_subset <- ic_subset %>%mutate(mag =nchar(value1) -1) %>%mutate(value =case_when( value1 == value2 ~round(value1), intensity_type =="number"& value1 ==0~1, intensity_type =="number"& value1 !=0~round_any(rowMeans(across(value1:value2)), 5* (10^ mag)), intensity_type =="percent"~round(rowMeans(across(value1:value2))),.default =NA )) %>%select(-c(mag, value1, value2))ic_append <- ic_subset %>%select(intensity_category_id, intensity_name, intensity_value, value) %>%rename(intensity_cat = intensity_value, intensity = value)status <- status %>%left_join(ic_append, by =c("intensity_category_id", "intensity_value"="intensity_cat")) %>%select(-intensity_category_id)
Identifying issues related to reported intensity values
There were 2 instances since October 2024 where an observer did not report “yes” for a particular phenohpase, but did report an intensity value.
Table 1: Observations with an intensity value, where phenophase state was not positive.
Site name
State
Species
Observation date
Phenophase
Status
Intensity
Bayton Loop Preserve
TX
American beautyberry
2024-12-21
ripe fruit
-1
Less than 5%
Pollinator Garden
TX
butterfly milkweed
2025-05-23
flower
-1
3 to 10
To explore whether observers were counting the number of flowers rather than the number of inflorescences, we identified observations where the reported intensity value was in the highest two categories available for that species.
Table 2: Observations of flowers with high reported intensity values by water year.
2024
2025
Species
Phenophase
Intensity category
Observations
Plants
Observations
Plants
American beautyberry
flower
1,001 to 10,000
4
4
12
7
American beautyberry
flower
More than 10,000
0
0
1
1
American star-thistle
flower
101 to 1,000
0
0
3
1
American star-thistle
flower
More than 1,000
0
0
1
1
Canada goldenrod
flower
101 to 1,000
0
0
1
1
Texas lupine
flower
101 to 1,000
0
0
5
2
West Indian shrubverbena
flower
1,001 to 10,000
0
0
9
1
blackeyed Susan
flower
More than 1,000
0
0
1
1
blue mistflower
flower
101 to 1,000
0
0
16
8
butterfly milkweed
flower
101 to 1,000
0
0
2
1
button eryngo
flower
101 to 1,000
4
1
0
0
cardinalflower
flower
101 to 1,000
1
1
0
0
common buttonbush
flower
1,001 to 10,000
15
6
11
4
common sunflower
flower
101 to 1,000
0
0
8
2
eastern baccharis
flower
1,001 to 10,000
6
3
3
1
eastern baccharis
flower
More than 10,000
0
0
2
2
eastern redbud
flower
1,001 to 10,000
4
3
21
9
firewheel
flower
101 to 1,000
0
0
7
3
horsetail milkweed
flower
101 to 1,000
3
1
7
2
lemon beebalm
flower
101 to 1,000
0
0
1
1
mealycup sage
flower
101 to 1,000
0
0
11
4
palmleaf thoroughwort
flower
101 to 1,000
0
0
35
5
purple passionflower
flower
101 to 1,000
2
1
5
1
red maple
flower
1,001 to 10,000
75
13
89
14
red maple
flower
More than 10,000
6
1
2
1
rubber rabbitbrush
flower
1,001 to 10,000
14
3
15
3
rubber rabbitbrush
flower
More than 10,000
8
4
0
0
seaside goldenrod
flower
101 to 1,000
0
0
5
1
seaside goldenrod
flower
More than 1,000
0
0
4
1
spider milkweed
flower
101 to 1,000
0
0
5
1
straggler daisy
flower
101 to 1,000
0
0
7
3
straggler daisy
flower
More than 1,000
0
0
1
1
turkey tangle fogfruit
flower
101 to 1,000
0
0
20
6
turkey tangle fogfruit
flower
More than 1,000
0
0
1
1
white crownbeard
flower
101 to 1,000
0
0
11
2
wild bergamot
flower
101 to 1,000
1
1
7
2
Identifying inconsistent phenophase status reports
We wanted to identify when observers provided incompatible status reports for different phenophases. In particular, we identified when observers reported a “no” to flowers but reported a “yes” or “?” to open flowers. Similarly, we identified when observers reported a “no” to fruits but reported a “yes” or “?” to ripe fruits. We also identified when observers reported a “?” to flowers but reported a “yes” to open flowers, and when observers reported a “?” to fruits but reported a “yes” to ripe fruits.
Code
# To look at this, can't have more than one observation of a plant per person# per day. We've already removed duplicates, but now need to resolve instances # where somebody made multiple observations of the same plant on the same date # that differed in some way.# For now, will keep record with more advanced phenophase or higher # intensity value. Will do this by sorting observations in descending# order and keeping only the first inddateobsp <- status %>%group_by(common_name, individual_id, obsdate, person_id, php) %>%summarize(n_obs =n(),.groups ="keep") %>%data.frame() inddateobsp$obsnum <-1:nrow(inddateobsp) status <- status %>%arrange(person_id, individual_id, obsdate, php, desc(phenophase_status), desc(intensity)) %>%left_join(select(inddateobsp, -c(n_obs, common_name)), by =c("person_id", "individual_id", "obsdate", "php")) %>%# Create "dups" column, where dups > 1 indicates that the observation can be# removed since there's another observation that same day with more advanced# phenology or higher intensity/abundance.mutate(dups =sequence(rle(as.character(obsnum))$lengths))# Remove extra observations and unnecessary columns status <- status %>%filter(dups ==1) %>%select(-c(obsnum, dups)) %>%arrange(common_name, obsdate, person_id, php)# To identify inconsistent status values, will need to put flower/fruit data # into wide form (all data for a plant visit in the same row). Removing# unknown status observations first (<0.5% of fruit/flower observations).statusw <- status %>%filter(php !="leaves") %>%filter(phenophase_status !=-1) %>%select(person_id, partner_group, site_id, state, common_name, individual_id, wy, obsdate, php, phenophase_status, intensity) %>%rename(status = phenophase_status) %>%pivot_wider(names_from = php,names_glue ="{php}_{.value}",values_from =c(status, intensity)) %>%data.frame()# Identify phenophase status inconsistencies# NOTE: changing NAs to 999 in order to make this code simplerstatusw <- statusw %>%mutate(across(contains("status"), ~replace_na(., 999))) %>%# Problem: flower = 0, open = NA or 1mutate(flower0_openNot0 =ifelse(flower_status ==0& open.flower_status !=0, 1, 0)) %>%# Problem: flower = NA, open = 1mutate(flowerNA_open1 =ifelse(flower_status ==999& open.flower_status ==1,1, 0)) %>%# Problem: fruit = 0, ripe = NA or 1mutate(fruit0_ripeNot0 =ifelse(fruit_status ==0& ripe.fruit_status !=0, 1, 0)) %>%# Problem: fruit = NA, ripe = 1mutate(fruitNA_ripe1 =ifelse(fruit_status ==999& ripe.fruit_status ==1,1, 0))# Table summarizing problems with flower/open flower statusflower_probs <- statusw %>%group_by(common_name, wy) %>%summarize(n =n(),n_flower0 =sum(flower_status ==0),n_flower1 =sum(flower_status ==1),n_flowerNA =sum(flower_status ==999),n_open0 =sum(open.flower_status ==0),n_open1 =sum(open.flower_status ==1),n_openNA =sum(open.flower_status ==999),n_flower0_openNot0 =sum(flower0_openNot0 ==1),n_flowerNA_open1 =sum(flowerNA_open1 ==1),.groups ="keep") %>%filter(n_flower0_openNot0 + n_flowerNA_open1 >0) %>%data.frame()# Table summarizing problems with fruit/ripe fruit statusfruit_probs <- statusw %>%group_by(common_name, wy) %>%summarize(n =n(),n_fruit0 =sum(fruit_status ==0),n_fruit1 =sum(fruit_status ==1),n_fruitNA =sum(fruit_status ==999),n_ripe0 =sum(ripe.fruit_status ==0),n_ripe1 =sum(ripe.fruit_status ==1),n_ripeNA =sum(ripe.fruit_status ==999),n_fruit0_ripeNot0 =sum(fruit0_ripeNot0 ==1),n_fruitNA_ripe1 =sum(fruitNA_ripe1 ==1),.groups ="keep") %>%filter(n_fruit0_ripeNot0 + n_fruitNA_ripe1 >0) %>%data.frame()
Table 3: Inconsistent reports of flowering phenophase statuses by species and water year
Species
Water year
No. observations
Flowers:no AND Open flowers:yes/?
Flowers:? AND Open flowers:yes
American beautyberry
2024
585
0
28
American beautyberry
2025
1106
13
22
American star-thistle
2025
44
1
0
Texas lupine
2025
179
3
0
West Indian shrubverbena
2025
106
4
1
blackeyed Susan
2025
199
2
0
blue mistflower
2025
166
1
1
common buttonbush
2024
300
2
0
common buttonbush
2025
359
1
0
common sunflower
2024
158
1
0
eastern baccharis
2025
83
1
0
eastern purple coneflower
2024
34
1
0
eastern purple coneflower
2025
265
0
1
eastern redbud
2024
462
4
2
eastern redbud
2025
543
5
2
horsetail milkweed
2024
151
1
1
mealycup sage
2025
165
1
0
purple prairie clover
2024
250
0
17
purple prairie clover
2025
176
0
17
red maple
2024
497
1
0
red maple
2025
369
2
1
rubber rabbitbrush
2024
181
3
0
rubber rabbitbrush
2025
188
1
0
wax mallow
2025
366
4
0
Table 4: Inconsistent reports of fruiting phenophase statuses
Species
Water year
No. observations
Fruit:no AND Ripe fruit:yes/?
Fruit:? AND Ripe fruit:yes
American beautyberry
2025
1106
2
0
American star-thistle
2025
44
1
0
Texas lupine
2025
179
2
0
West Indian shrubverbena
2025
106
3
0
blackeyed Susan
2024
1
1
0
blackeyed Susan
2025
199
12
0
blue mistflower
2025
166
5
1
common buttonbush
2024
300
1
0
common buttonbush
2025
359
1
0
common milkweed
2025
15
1
0
common sunflower
2024
158
4
0
common sunflower
2025
201
1
0
eastern baccharis
2024
114
1
0
eastern purple coneflower
2025
265
1
1
eastern redbud
2024
462
1
0
eastern redbud
2025
543
14
0
firewheel
2025
117
5
1
green antelopehorn
2025
199
1
0
horsetail milkweed
2024
151
1
0
horsetail milkweed
2025
145
2
0
lemon beebalm
2025
10
1
0
mealycup sage
2025
165
0
1
purple passionflower
2025
27
1
0
rubber rabbitbrush
2025
188
2
0
spider milkweed
2025
57
1
0
turkey tangle fogfruit
2025
87
2
0
wax mallow
2025
366
2
0
white crownbeard
2025
250
2
0
wild bergamot
2025
136
6
1
Summary of sites monitored
Code
# Plot locations where flowering/fruiting or milkweed leaves observed in current# water year (Oct 2024 to present)status <- status %>%mutate(ind_date =paste0(individual_id, "_", obsdate)) locs <- status %>%filter(wy ==2025) %>%group_by(site_id, lat, lon, state) %>%summarize(nspp =n_distinct(common_name),nplants =n_distinct(individual_id),nobservers =n_distinct(person_id),# nobs: Number of observations, where an observations is all# data (all phenophases) submitted for a plant on given datenobs =n_distinct(ind_date), .groups ="keep") %>%data.frame()locs_by_state_int <- locs %>%group_by(state) %>%summarize(nspp_per_site =round(mean(nspp), 2),nplants_per_site =round(mean(nplants), 2),nobs_per_site =round(mean(nobs),2)) %>%data.frame()locs_by_state <- status %>%filter(wy ==2025) %>%group_by(state) %>%summarize(nsites =n_distinct(site_id),nspp =n_distinct(common_name),nplants =n_distinct(individual_id),nobs =n_distinct(ind_date)) %>%data.frame() %>%left_join(locs_by_state_int, by ="state")
Figure 1: Locations of sites where priority species were monitored between October 2024 and present.
Figure 2: Locations of sites in Louisiana where priority species were monitored between October 2024 and present.
Figure 3: Locations of sites in New Mexico where priority species were monitored between October 2024 and present.
Figure 4: Locations of sites in Oklahoma where priority species were monitored between October 2024 and present.
Figure 5: Locations of sites in Texas where priority species were monitored between October 2024 and present.